Skip to main content

07 FIR实验

Finite Impulse Response 有限冲激响应

up的视频讲解: https://space.bilibili.com/1455872040

up视频里的实验代码: 👣FIR实验代码

FIR驱动代码: 👣FIR驱动代码

1 🌱 FIR 简介​

1 滤波就是有选择地保留变化​

上一节我们把声音拆成了不同频率。接下来,我们来做一个更实际的操作:保留变化较慢的部分,让变化较快的部分减弱。

最容易理解的方法就是平均。假设ADC每次读数都会有一点抖动,我们把最近4次的数据加起来再除以4,结果通常会更平滑。但平滑也有代价:真实信号的快速变化同样会被减弱,输出还会落后于输入。

FIR把这个思路推广成加权求和:

y[n] = b[0]x[n] + b[1]x[n-1] + … + b[M-1]x[n-M+1]

x是采样值,b是系数,M是系数个数,也叫抽头数。M个抽头对应M−1阶。输入一个1、后面全是0,在有限的M个输出后就归零,因此叫有限冲激响应。

2 ➡️先手写一个4点平均​

下面的函数每收到一个点调用一次,不能每来一帧就清空历史。

static float history[4] = {0};
static unsigned int pos = 0;

float Mean4_Process(float x)
{
float sum = 0.0f;
history[pos] = x;
pos = (pos + 1U) % 4U;
for (unsigned int i = 0; i < 4U; ++i) sum += history[i];
return sum * 0.25f;
}

开机时历史为0,所以恒定输入1得到 0.25、0.5、0.75、1、1……。这是启动暂态,不是ADC突然变低了。4点平均在8 kHz采样下会消除整周期的2 kHz分量,但它的阻带并不平坦,不能代替经过指标设计的低通滤波器。

3 🔧设计一个31抽头低通​

本节独立使用8 kHz采样,输入为500 Hz与2000 Hz正弦相加,设计31抽头、1000 Hz截止参数的Hamming窗低通。主课Q15 FFT工程为5 kHz,接真实ADC时需将TIM3的ARR由199改成124(PSC仍为71,定时器时钟72 MHz)。我们要观察500 Hz基本保留、2000 Hz明显减弱。

系数由下面的Python代码生成,也可下载设计与验证脚本。电脑端需要NumPy、SciPy;单片机不需要Python。

from scipy import signal
b = signal.firwin(31, 1000, fs=8000, window="hamming")
print(", ".join(f"{v:.10e}f" for v in b[::-1]))

这里的1000 Hz是窗函数设计的半幅点附近,约−6 dB;不要和下一节Butterworth的−3 dB截止频率混为一谈。SciPy firwin参数定义

31个对称系数形成线性相位FIR,群延迟为 (31−1)/2 = 15 个采样,即 15/8000 = 1.875 ms。若每256点才处理一次,还会有分块等待时间。

课程FIR和IIR设计的频率响应及冲激响应

这是系数计算得到的理论曲线。蓝色FIR在2000 Hz处约衰减59.25 dB;右侧可以看到它的冲激响应在31点后结束。橙色是下一节的二阶IIR,用来对照“有限”和“无限”的区别。

4 💻CMSIS-DSP实现​

使用FFT章节相同的Cortex-M3工程配置。将下载的系数头文件放进工程包含目录,或者运行脚本自行生成。

CMSIS的FIR系数按时间反序存放。本例系数对称,倒序后恰好一样,但非对称滤波器不能省略这个检查。Cortex-M3标量f32版本的状态数组长度为 numTaps + blockSize − 1;初始化只做一次。Arm FIR接口说明

#include "arm_math.h"
#include "filter_coeffs.h"
#include <math.h>

#define BLOCK_SIZE 256U
#define PI_F 3.14159265358979323846f
static arm_fir_instance_f32 fir;
static float32_t fir_state[FIR_TAPS + BLOCK_SIZE - 1U];
static float32_t input[BLOCK_SIZE], output[BLOCK_SIZE];
static uint32_t sample_index = 0;

void FIR_Experiment_Init(void)
{
arm_fir_init_f32(&fir, FIR_TAPS, fir_coeffs, fir_state, BLOCK_SIZE);
}

void FIR_Experiment_Block(void)
{
for (uint32_t i = 0; i < BLOCK_SIZE; ++i) {
float t = (float)(sample_index % 8000U) / 8000.0f;
input[i] = sinf(2.0f * PI_F * 500.0f * t)
+ 0.5f * sinf(2.0f * PI_F * 2000.0f * t);
++sample_index;
}
arm_fir_f32(&fir, input, output, BLOCK_SIZE);
}

在main初始化后调用一次 FIR_Experiment_Init(),再多次调用 FIR_Experiment_Block()。初次验证时可在一次处理后暂停、用调试器导出input/output;若用串口观察,先保存结果再低速发送,发送时间不计入连续实时处理能力。

算法会自动保存跨块的历史值。不需要自己把上一帧末尾31点再塞进输入,否则会重复处理。

5 🔌换成真实ADC数据​

先按FFT章节配置TIM3、ADC1与DMA,再将TIM3的ARR改为124,使本节实际采样率为8 kHz。第一步可以用单帧数据验证低通,但要注意:停采产生的时间空档不能当作连续时间的一部分。

  • 独立单帧演示:每次采集都视为一次新实验,重新初始化状态,并舍弃前30个暂态样本。
  • 连续波形滤波:使用循环DMA,按真实时间顺序处理每个半缓冲,只在启动时初始化滤波器。

把输入生成部分替换成ADC码值转换,例如 input[i] = ((float)adc[i] - 2048.0f) / 2048.0f。这里2048只是理想中点,要用实测静音偏置校准;低通不会自动消除直流。

暂时不要每帧减去不同的均值再做连续滤波,这会人为改变帧边界。要消除慢漂移,可以设计连续高通,而不是把每一帧当成互不相干的数据。

6 ✅怎样验收​

检查应看到的结果
输入冲激 1,0,0…输出依次等于设计系数,31点之后为0
输入恒定1过渡后接近1,因为低通直流增益为1
500 Hz单音稳态幅度接近输入
2000 Hz单音稳态幅度显著减小,具体数值看设计脚本输出
500+2000 Hz混合波形更平滑,频谱里的2000 Hz峰下降
一次处理与分块处理比较初始化相同、顺序相同时输出应一致

比较输入输出波形时,先考虑15点延迟,不能拿同一下标直接做差就说滤波错了。做频谱比较时,使用相同采样率、窗函数、点数和幅度归一化,并避开启动暂态。

7 常见问题与练习​

输出每256点跳一下,先检查是否反复清零状态、DMA覆盖或漏块。低通之后还是有高频,先看实际频响,有限长度滤波器的过渡带不是一条竖线。

把抽头数改成63并重新生成系数,比较频响、RAM和计算时间。再改变截止参数,观察“更平滑”和“更能保留细节”之间的取舍。不要只增加数组长度,却仍然使用原来的31个系数。

下一节我们使用输出的历史值做反馈,看看IIR滤波器如何用更少的系数达到低通效果。